package com.carey.blog.repository.impl;

import static com.google.appengine.api.datastore.FetchOptions.Builder.withOffset;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.carey.blog.framework.latke.Keys;
import com.carey.blog.framework.latke.model.Pagination;
import com.carey.blog.framework.latke.repository.RepositoryException;
import com.carey.blog.framework.latke.repository.gae.AbstractGAERepository;
import com.carey.blog.model.Article;
import com.carey.blog.model.BlogSync;
import com.carey.blog.repository.ArticleRepository;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultList;

/**
 * Article Google App Engine repository.
 * 
 */
public final class ArticleGAERepository extends AbstractGAERepository implements
		ArticleRepository {

	/**
	 * Logger.
	 */
	private static final Logger LOGGER = Logger
			.getLogger(ArticleGAERepository.class.getName());
	/**
	 * Key of the most comment articles cache count.
	 */
	private static final String KEY_MOST_CMT_ARTICLES_CACHE_CNT = "mostCmtArticlesCacheCnt";
	/**
	 * Key of the recent articles cache count.
	 */
	private static final String KEY_RECENT_ARTICLES_CACHE_CNT = "mostRecentArticlesCacheCnt";

	@Override
	public String getName() {
		return Article.ARTICLE;
	}

	@Override
	public JSONObject getByAuthorEmail(final String authorEmail,
			final int currentPageNum, final int pageSize)
			throws RepositoryException {
		final Query query = new Query(getName());
		
		query.addFilter(Article.ARTICLE_AUTHOR_EMAIL,
				Query.FilterOperator.EQUAL, authorEmail);
		query.addSort(Article.ARTICLE_UPDATE_DATE,
				Query.SortDirection.ASCENDING);

		final PreparedQuery preparedQuery = getDatastoreService()
				.prepare(query);
		final int count = preparedQuery.countEntities(FetchOptions.Builder
				.withDefaults());
		final int pageCount = (int) Math.ceil((double) count
				/ (double) pageSize);

		final JSONObject ret = new JSONObject();
		final JSONObject pagination = new JSONObject();
		try {
			ret.put(Pagination.PAGINATION, pagination);
			pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
			final int offset = pageSize * (currentPageNum - 1);
			final QueryResultList<Entity> queryResultList = preparedQuery
					.asQueryResultList(withOffset(offset).limit(pageSize));
			final JSONArray results = new JSONArray();
			ret.put(Keys.RESULTS, results);
			for (final Entity entity : queryResultList) {
				final JSONObject jsonObject = entity2JSONObject(entity);

				results.put(jsonObject);
			}
		} catch (final JSONException e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new RepositoryException(e);
		}

		return ret;
	}

	@Override
	public JSONObject getByPermalink(final String permalink) {
		final String cacheKey = "getByPermalink[" + permalink + "]";
		JSONObject ret = (JSONObject) CACHE.get(cacheKey);

		if (null == ret) {
			final Query query = new Query(getName());
			query.addFilter(Article.ARTICLE_PERMALINK,
					Query.FilterOperator.EQUAL, permalink);
			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final Entity entity = preparedQuery.asSingleEntity();
			if (null == entity) {
				return null;
			}

			final Map<String, Object> properties = entity.getProperties();

			ret = new JSONObject(properties);

			CACHE.put(cacheKey, ret);
		}

		return ret;
	}

	@Override
	public List<JSONObject> getRecentArticles(final int fetchSize) {
		final String cacheKey = KEY_RECENT_ARTICLES_CACHE_CNT + "[" + fetchSize
				+ "]";
		
		@SuppressWarnings("unchecked")
		List<JSONObject> ret = (List<JSONObject>) CACHE.get(cacheKey);
		
		if (null != ret) {
			LOGGER.log(Level.FINEST, "Got the recent articles from cache");
		} else {
			ret = new ArrayList<JSONObject>();
			final Query query = new Query(getName());
			query.addSort(Article.ARTICLE_UPDATE_DATE,
					Query.SortDirection.DESCENDING);
			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final QueryResultIterable<Entity> queryResultIterable = preparedQuery
					.asQueryResultIterable(FetchOptions.Builder
							.withLimit(fetchSize));

			for (final Entity entity : queryResultIterable) {
				final JSONObject article = entity2JSONObject(entity);
				ret.add(article);
			}

			CACHE.put(cacheKey, ret);

			LOGGER.log(Level.FINEST,
					"Got the recent articles, then put it into cache");
		}

		return ret;
	}

	@Override
	public List<JSONObject> getMostCommentArticles(final int num) {
		final String cacheKey = KEY_MOST_CMT_ARTICLES_CACHE_CNT + "[" + num
				+ "]";
		
		@SuppressWarnings("unchecked")
		List<JSONObject> ret = (List<JSONObject>) CACHE.get(cacheKey);
		
		if (null != ret) {
			LOGGER.log(Level.FINEST, "Got the most comment articles from cache");
		} else {
			ret = new ArrayList<JSONObject>();
			final Query query = new Query(getName());
			query.addSort(Article.ARTICLE_COMMENT_COUNT,
					Query.SortDirection.DESCENDING)
					.addSort(Article.ARTICLE_UPDATE_DATE,
							Query.SortDirection.DESCENDING);
			query.addFilter(Article.ARTICLE_IS_PUBLISHED,
					Query.FilterOperator.EQUAL, true);
			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final QueryResultIterable<Entity> queryResultIterable = preparedQuery
					.asQueryResultIterable(FetchOptions.Builder.withLimit(num));

			for (final Entity entity : queryResultIterable) {
				final JSONObject article = entity2JSONObject(entity);
				ret.add(article);
			}

			CACHE.put(cacheKey, ret);

			LOGGER.log(Level.FINEST,
					"Got the most comment articles, then put it into cache");
		}

		return ret;
	}

	@Override
	public List<JSONObject> getMostViewCountArticles(final int num) {
		final String cacheKey = KEY_MOST_CMT_ARTICLES_CACHE_CNT + "[" + num
				+ "]";
		
		@SuppressWarnings("unchecked")
		List<JSONObject> ret = (List<JSONObject>) CACHE.get(cacheKey);
		
		if (null != ret) {
			LOGGER.log(Level.FINEST, "Got the most viewed articles from cache");
		} else {
			ret = new ArrayList<JSONObject>();
			final Query query = new Query(getName());
			query.addSort(Article.ARTICLE_VIEW_COUNT,
					Query.SortDirection.DESCENDING)
					.addSort(Article.ARTICLE_UPDATE_DATE,
							Query.SortDirection.DESCENDING);
			query.addFilter(Article.ARTICLE_IS_PUBLISHED,
					Query.FilterOperator.EQUAL, true);
			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final QueryResultIterable<Entity> queryResultIterable = preparedQuery
					.asQueryResultIterable(FetchOptions.Builder.withLimit(num));

			for (final Entity entity : queryResultIterable) {
				final JSONObject article = entity2JSONObject(entity);
				ret.add(article);
			}

			CACHE.put(cacheKey, ret);

			LOGGER.log(Level.FINEST,
					"Got the most viewed articles, then put it into cache");
		}

		return ret;
	}

	@Override
	public JSONObject getPreviousArticle(final String articleId)
			throws RepositoryException {
		try {
			final JSONObject current = get(articleId);
			final Date currentDate = (Date) current
					.get(Article.ARTICLE_CREATE_DATE);

			final Query query = new Query(getName());
			query.addFilter(Article.ARTICLE_CREATE_DATE,
					Query.FilterOperator.LESS_THAN, currentDate);
			query.addFilter(Article.ARTICLE_IS_PUBLISHED,
					Query.FilterOperator.EQUAL, true);
			query.addSort(Article.ARTICLE_CREATE_DATE,
					Query.SortDirection.DESCENDING);

			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final List<Entity> result = preparedQuery
					.asList(FetchOptions.Builder.withLimit(1));

			if (1 == result.size()) {
				try {
					final JSONObject ret = new JSONObject();
					final Entity article = result.get(0);
					ret.put(Article.ARTICLE_TITLE,
							article.getProperty(Article.ARTICLE_TITLE));
					ret.put(Article.ARTICLE_PERMALINK,
							article.getProperty(Article.ARTICLE_PERMALINK));

					return ret;
				} catch (final JSONException e) {
					LOGGER.log(Level.SEVERE, e.getMessage(), e);
				}
			}

			return null;
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new RepositoryException(e);
		}
	}

	@Override
	public JSONObject getNextArticle(final String articleId)
			throws RepositoryException {
		try {
			final JSONObject current = get(articleId);
			final Date currentDate = (Date) current
					.get(Article.ARTICLE_CREATE_DATE);
			final Query query = new Query(getName());
			query.addFilter(Article.ARTICLE_CREATE_DATE,
					Query.FilterOperator.GREATER_THAN, currentDate);
			query.addFilter(Article.ARTICLE_IS_PUBLISHED,
					Query.FilterOperator.EQUAL, true);
			query.addSort(Article.ARTICLE_CREATE_DATE,
					Query.SortDirection.ASCENDING);

			final PreparedQuery preparedQuery = getDatastoreService().prepare(
					query);
			final List<Entity> result = preparedQuery
					.asList(FetchOptions.Builder.withLimit(1));

			if (1 == result.size()) {
				try {
					final JSONObject ret = new JSONObject();
					final Entity article = result.get(0);
					ret.put(Article.ARTICLE_TITLE,
							article.getProperty(Article.ARTICLE_TITLE));
					ret.put(Article.ARTICLE_PERMALINK,
							article.getProperty(Article.ARTICLE_PERMALINK));

					return ret;
				} catch (final JSONException e) {
					LOGGER.log(Level.SEVERE, e.getMessage(), e);
				}
			}

			return null;
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new RepositoryException(e);
		}
	}

	@Override
	public void importArticle(final JSONObject article)
			throws RepositoryException {
		String articleId = null;
		try {
			if (!article.has(Keys.OBJECT_ID)) {
				throw new RepositoryException(
						"The article to import MUST exist " + "id");
			}
			articleId = article.getString(Keys.OBJECT_ID);

			if (!article.has(Article.ARTICLE_CREATE_DATE)) {
				throw new RepositoryException(
						"The article to import MUST exist " + "create date");
			}

			// XXX: check other params

			// Remove external attributes, such as "blogSyncExternal...."
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_ABSTRACT);
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_CATEGORIES);
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_CONTENT);
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_CREATE_DATE);
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_ID);
			article.remove(BlogSync.BLOG_SYNC_EXTERNAL_ARTICLE_TITLE);

			super.add(article);
		} catch (final Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw new RepositoryException(e);
		}

		LOGGER.log(Level.FINER, "Imported an article[oId={0}]", articleId);
	}

	@Override
	public boolean isPublished(final String articleId)
			throws RepositoryException {
		final JSONObject article = get(articleId);
		if (null == article) {
			return false;
		} else {
			try {
				return article.getBoolean(Article.ARTICLE_IS_PUBLISHED);
			} catch (final JSONException e) {
				LOGGER.log(Level.SEVERE, e.getMessage(), e);

				throw new RepositoryException(e);
			}
		}
	}

	@Override
	public List<JSONObject> getRandomly(final int fetchSize)
			throws RepositoryException {
		final List<JSONObject> ret = new ArrayList<JSONObject>();
		if (0 == count()) {
			return ret;
		}

		final double mid = Math.random();
		LOGGER.log(Level.FINEST, "Random mid[{0}]", mid);

		Query query = new Query(getName());
		query.addFilter(Article.ARTICLE_RANDOM_DOUBLE,
				Query.FilterOperator.GREATER_THAN_OR_EQUAL, mid);
		query.addFilter(Article.ARTICLE_RANDOM_DOUBLE,
				Query.FilterOperator.LESS_THAN_OR_EQUAL, 1D);
		query.addFilter(Article.ARTICLE_IS_PUBLISHED,
				Query.FilterOperator.EQUAL, true);
		
		PreparedQuery preparedQuery = getDatastoreService().prepare(query);
		QueryResultList<Entity> entities = preparedQuery
				.asQueryResultList(FetchOptions.Builder.withLimit(fetchSize));

		for (final Entity entity : entities) {
			final JSONObject jsonObject = entity2JSONObject(entity);
			ret.add(jsonObject);
		}

		final int reminingSize = fetchSize - entities.size();
		if (0 != reminingSize) { // Query for remains
			query = new Query(getName());
			query.addFilter(Article.ARTICLE_RANDOM_DOUBLE,
					Query.FilterOperator.GREATER_THAN_OR_EQUAL, 0D);
			query.addFilter(Article.ARTICLE_RANDOM_DOUBLE,
					Query.FilterOperator.LESS_THAN_OR_EQUAL, mid);
			query.addFilter(Article.ARTICLE_IS_PUBLISHED,
					Query.FilterOperator.EQUAL, true);
			preparedQuery = getDatastoreService().prepare(query);
			entities = preparedQuery.asQueryResultList(FetchOptions.Builder
					.withLimit(reminingSize));
			for (final Entity entity : entities) {
				final JSONObject jsonObject = entity2JSONObject(entity);
				ret.add(jsonObject);
			}
		}

		return ret;
	}

	/**
	 * Gets the {@link ArticleGAERepository} singleton.
	 * 
	 * @return the singleton
	 */
	public static ArticleGAERepository getInstance() {
		return SingletonHolder.SINGLETON;
	}

	/**
	 * Private default constructor.
	 */
	private ArticleGAERepository() {
	}

	/**
	 * Singleton holder.
	 * 
	 */
	private static final class SingletonHolder {

		/**
		 * Singleton.
		 */
		private static final ArticleGAERepository SINGLETON = new ArticleGAERepository();

		/**
		 * Private default constructor.
		 */
		private SingletonHolder() {
		}
	}
}
